package com.kalessil.phpStorm.phpInspectionsEA.inspectors.security;

import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.jetbrains.php.lang.lexer.PhpTokenTypes;
import com.jetbrains.php.lang.psi.elements.*;
import com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor;
import com.kalessil.phpStorm.phpInspectionsEA.utils.*;
import org.jetbrains.annotations.NotNull;

import java.util.HashSet;
import java.util.Set;

/*
 * This file is part of the Php Inspections (EA Extended) package.
 *
 * (c) Vladimir Reznichenko <kalessil@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

public class PotentialMalwareInspector extends LocalInspectionTool {
    private static final String messageEval     = "This eval looks pretty much as some malware.";
    private static final String messageFileHide = "This looks pretty much as some malware hiding files manipulation.";
    private static final String messageDecode   = "This function looks pretty much as part of some malware.";
    private static final String messageCall     = "This function looks pretty much as part of some malware.";

    private static final HashSet<String> evalSuspects   = new HashSet<>();
    private static final HashSet<String> decodeSuspects = new HashSet<>();
    static {
        evalSuspects.add("base64_decode");
        evalSuspects.add("gzinflate");
        evalSuspects.add("gzuncompress");
        evalSuspects.add("urldecode");
        evalSuspects.add("curl_unescape");
        evalSuspects.add("strrev");
        evalSuspects.add("str_rot13");
        evalSuspects.add("file_get_contents");
        evalSuspects.add("curl_exec");

        decodeSuspects.add("base64_decode");
        decodeSuspects.add("gzinflate");
        decodeSuspects.add("gzuncompress");
        decodeSuspects.add("urldecode");
        decodeSuspects.add("curl_unescape");
    }

    @NotNull
    @Override
    public String getShortName() {
        return "PotentialMalwareInspection";
    }

    @NotNull
    @Override
    public String getDisplayName() {
        return "Basic malware patterns";
    }

    @Override
    @NotNull
    public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
        return new BasePhpElementVisitor() {
            @Override
            public void visitPhpFunctionCall(@NotNull FunctionReference reference) {
                final String functionName = reference.getName();
                if (functionName != null) {
                    if (functionName.equals("touch")) {
                        final PsiElement[] arguments = reference.getParameters();
                        if (arguments.length == 3) {
                            holder.registerProblem(
                                    reference,
                                    MessagesPresentationUtil.prefixWithEa(messageFileHide)
                            );
                        }
                    } else if (functionName.equals("get_defined_functions")) {
                        holder.registerProblem(
                                reference,
                                MessagesPresentationUtil.prefixWithEa(messageCall)
                        );
                    }
                }
            }

            @Override
            public void visitPhpEval(@NotNull PhpEval eval) {
                /* get through "(...)" and "@..." constructs */
                PsiElement argument = eval.getFirstPsiChild();
                /* TODO: merge into getExpressionThruParenthesises */
                while (argument instanceof UnaryExpression || argument instanceof ParenthesizedExpression) {
                    if (argument instanceof ParenthesizedExpression) {
                        argument = ((ParenthesizedExpression) argument).getArgument();
                        continue;
                    }

                    final UnaryExpression unaryArgument = (UnaryExpression) argument;
                    if (OpenapiTypesUtil.is(unaryArgument.getOperation(), PhpTokenTypes.opSILENCE)) {
                        argument = unaryArgument.getValue();
                        continue;
                    }

                    break;
                }

                /* if argument is not a function call or not a target function, terminate inspection */
                if (!(argument instanceof FunctionReference)) {
                    return;
                }
                final String name = ((FunctionReference) argument).getName();
                if (name == null || !evalSuspects.contains(name.toLowerCase())) {
                    return;
                }

                /* eval(file_get_contents('php://input')) is widely used as an interactive console */
                if (name.equalsIgnoreCase("file_get_contents")) {
                    final PsiElement[] params = ((FunctionReference) argument).getParameters();
                    if (params.length > 0 && params[0] instanceof StringLiteralExpression) {
                        return;
                    }
                }

                /* fire a warning message */
                holder.registerProblem(
                        eval,
                        MessagesPresentationUtil.prefixWithEa(messageEval)
                );
            }

            @Override
            public void visitPhpFunction(@NotNull Function function) {
                /* we expect return to be the last statement */
                final GroupStatement body      = ExpressionSemanticUtil.getGroupStatement(function);
                final PsiElement lastStatement = null == body ? null : ExpressionSemanticUtil.getLastStatement(body);
                final PsiElement nameNode      = NamedElementUtil.getNameIdentifier(function);
                if (null == nameNode || !(lastStatement instanceof PhpReturn)) {
                    return;
                }
                /* we need 2+ expressions in the body */
                if (ExpressionSemanticUtil.countExpressionsInGroup(body) < 2) {
                    return;
                }

                /* decode function is hidden */
                PsiElement returnArgument = ExpressionSemanticUtil.getReturnValue((PhpReturn) lastStatement);
                /* TODO: merge into getExpressionThruParenthesises */
                while (returnArgument instanceof UnaryExpression || returnArgument instanceof ParenthesizedExpression) {
                    if (returnArgument instanceof ParenthesizedExpression) {
                        returnArgument = ((ParenthesizedExpression) returnArgument).getArgument();
                        continue;
                    }

                    final UnaryExpression unaryArgument = (UnaryExpression) returnArgument;
                    final PsiElement operator           = unaryArgument.getOperation();
                    if (null != operator && PhpTokenTypes.opSILENCE == operator.getNode().getElementType()) {
                        returnArgument = unaryArgument.getValue();
                        continue;
                    }

                    break;
                }

                /* we expect a function reference returned */
                if (!(returnArgument instanceof FunctionReference)) {
                    return;
                }
                final FunctionReference call = (FunctionReference) returnArgument;


                /* Case 1: analyze implicit function call */
                final String callName = call.getName();
                if (callName != null && decodeSuspects.contains(callName.toLowerCase())) {
                    holder.registerProblem(
                            nameNode,
                            MessagesPresentationUtil.prefixWithEa(messageDecode)
                    );
                    return;
                }


                /* Case 2: analyze variable function call */
                if (!(call.getFirstPsiChild() instanceof Variable)) {
                    return;
                }
                final Set<PsiElement> values = PossibleValuesDiscoveryUtil.discover(call.getFirstPsiChild());
                if (! values.isEmpty()) {
                    for (final PsiElement value : values) {
                        if (value instanceof StringLiteralExpression) {
                            final String name = ((StringLiteralExpression) value).getContents().toLowerCase();
                            if (decodeSuspects.contains(name)) {
                                holder.registerProblem(
                                        nameNode,
                                        MessagesPresentationUtil.prefixWithEa(messageDecode)
                                );
                                break;
                            }
                        }
                    }
                }
                values.clear();
            }
        };
    }
}
